'use strict';
const os = require("node:os");
const path = require('node:path');
const net = require('net');
const fs = require('fs');
const { exec } = require('node:child_process');
const express = require('express');
const expressWs = require('express-ws');
const cors = require('cors');
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads')

if (isMainThread) {
    const need_cmds = ['usbip', 'usbipd'];
    const modprobe_list = ['usbip-core', 'usbip-host', 'vhci-hcd']

    const sudo_str = "sudo"
    // const sudo_str = ""

    /* 
    sudo modprobe usbip-core
    sudo modprobe usbip-host
    sudo modprobe vhci-hcd
    */

    let sys_type = os.platform()
    let http_port = 0;
    let enable_log = false;
    let enable_auto_mount = false;
    let enable_only_server = false;
    const PROBE_INTERVAL = 10 * 1000;
    const cur_path = "./"
    const www_path = path.join(cur_path, 'public')
    let excluded_list = [];
    let ip_list = []
    let inBun = false;
    let usbip = {
        type: sys_type,
        servers_live_list: {},
        servers: {},
        devices: {
            remote: {},
            local: {},
        },
        port: {},
        bind: {}
    }

    console.log(process.argv)

    for (let i = 2; i < process.argv.length; i++) {
        if (process.argv[i] == '--port' || process.argv[i] == '-p') {
            i++;
            http_port = process.argv[i];
        } else if (process.argv[i] == '--log' || process.argv[i] == '-l') {
            enable_log = true;
        } else if (process.argv[i] == '--auto_mount' || process.argv[i] == '-a') {
            enable_auto_mount = true;
        } else if (process.argv[i] == '--server' || process.argv[i] == '-s') {
            enable_only_server = true;
        } else if (process.argv[i] == '--excluded' || process.argv[i] == '-e') {
            i++;
            fs.readFile(process.argv[i], 'utf8', (err, data) => {
                if (err) {
                    console.error(`Failed to open file ${process.argv[i]}`, err);
                    return;
                }
                try {
                    excluded_list = JSON.parse(data);
                    if (!Array.isArray(excluded_list))
                    {
                        excluded_list = [];
                        console.error("Data should be an array");
                    }
                    else
                        console.log("excluded list", excluded_list);
                } catch (err) {
                    console.error("Failed to json parse from", data);
                }
            });
        } else if (process.argv[i] == '-h' || process.argv[i] == '--help') {
            console.log('Usage: server --port|-p <http_port> [-l -a -s -e]');
            console.log('-p|--port          : WEB page port');
            console.log('-l|--log           : Enable log output');
            console.log('-a|--auto_mount    : auto mount local device');
            console.log('-s|--server        : Only servers are supported');
            console.log('-e|--excluded      : When automatically mounting, exclude USB devices at the specified address');
            process.exit(0);
        }
    }

    if (process.argv[0].indexOf('bun') != -1) {
        console.log('bun')
        inBun = true
    } else {
        console.log('node')
    }

    let wsClients = [];
    let exitTick = 0;

    const getLocalIPV4 = (ver = 4) => {
        const interfaces = os.networkInterfaces();
        let ips = [];
        for (let netDev in interfaces) {
            for (let netProt of interfaces[netDev]) {
                if (
                    netProt.family === `IPv${ver}` &&
                    !netProt.internal &&
                    netProt.address !== "127.0.0.1"
                ) ips.push(netProt.address);
            }
        }
        return ips;
    };
    const localIPV4 = getLocalIPV4();

    const app = express();
    expressWs(app);
    app.use(cors());
    app.use(express.static(www_path));
    app.use(express.json());
    app.use(express.urlencoded({ extended: false }))
    app.ws('/', function (ws, req) {
        ws_open(ws)
        ws.on('close', function () {
            ws_close(ws)
        })
        ws.on('message', (msg) => {
            if (enable_log)
                console.log("Websocket message:", msg);
            ws_message(ws, msg);
        });
        ws.on("error", function (msg) {
            ws_error(ws, msg);
        })
    });

    const server = app.listen(http_port, () => {
        let openUrl = undefined;
        localIPV4.forEach(async (item, index) => {
            if (localIPV4.length == 1 || (!openUrl && item.indexOf('192.168') != -1))
                openUrl = `http://${item}:${server.address().port}`;
            ip_list.push(item);
            console.log(`http://${item}:${server.address().port}`);
        });
        localIPV4.forEach(async (item, index) => {
            console.log(`ws://${item}:${server.address().port}`);
        });
        if (!openUrl)
            openUrl = `http://127.0.0.1:${server.address().port}`;
        openWebUrl(openUrl);
    });

    function openWebUrl(url) {
        if (enable_log)
            console.log(sys_type, url, www_path)
        switch (sys_type) {
            case 'win32':   //windows系统
                let cmd = `start msedge --args --enable-thread-compartment-tagging --app=${url}`
                console.log(cmd)
                exec(cmd)
                break
            case 'darwin':  //苹果系统
                exec(`open ${url}`)
                break
            default:  //linux系统
                console.log("当前程序依赖:", need_cmds)
                exec(`whereis modprobe`, (err, stdout, stderr) => {
                    for (let mod of modprobe_list) {
                        if (stdout.split(":").length > 1)
                            exec(`${sudo_str} modprobe ${mod}`)
                    }
                })
                exec(`${sudo_str} killall -9 usbipd`)
                exec(`${sudo_str} usbipd &`)
                exec("whereis xdg-open", (err, stdout, stderr) => {
                    if (stdout.split(":").length > 1)
                        exec(`xdg-open ${url}`)
                })
        }
    }

    if (!enable_only_server)
        setInterval(() => {
            if (wsClients.length == 0) {
                if (exitTick && Date.now() - exitTick > 30000) {
                    console.log('The page exits for more than 30 seconds and exits the program');
                    console.log('页面退出超过30秒, 退出程序');
                    process.exit();
                }
            }
        }, 3000);

    function ws_send_msg_to_all(msg) {
        for (let ws of wsClients) {
            ws.send(JSON.stringify(msg));
        }
    }

    function ws_open(ws) {
        console.log('WebSocket client connected');
        wsClients.push(ws);
        exitTick = 0;
        ws.send(JSON.stringify({ api: 'get_info', data: usbip }));
    }

    function ws_close(ws) {
        console.log('WebSocket client disconnected');
        wsClients = wsClients.filter(client => client !== ws);
        if (wsClients.length == 0)
            exitTick = Date.now();
    }
    function ws_error(ws, error) {
        console.error('WebSocket error', error);
    }

    function ws_message(ws, message) {
        let parsedMessage;
        try {
            parsedMessage = JSON.parse(message);
        } catch (error) {
            console.error('Invalid JSON message', error);
            ws.send(JSON.stringify({ api: 'error', data: 'Invalid JSON format' }));
            return;
        }

        console.log("ws recv msg:", parsedMessage)
        const { api, data } = parsedMessage;

        if (api === 'ping') {
            ws.send(JSON.stringify({ api: 'pong' }));
        } else if (api == "get_info") {
            ws.send(JSON.stringify({ api: "get_info", code: 0, data: usbip }));
        } else if (api == "attach") {
            const { ip, id } = data;
            if (!ip || !id) {
                ws.send(JSON.stringify({ api: "attach", code: -1, data: 'Invalid parameter' }));
                return;
            }
            usbip_attach(ip, id, (err) => {
                if (err) {
                    ws.send(JSON.stringify({ api: "attach", code: -1, data: err }));
                    return;
                }
                ws.send(JSON.stringify({ api: "attach", code: 0 }));
            })
        } else if (api == "detach") {
            if (!data) {
                ws.send(JSON.stringify({ api: "detach", code: -1, data: 'Invalid parameter' }));
                return;
            }
            usbip_detach(data, (err) => {
                if (err) {
                    ws.send(JSON.stringify({ api: "detach", code: -1, data: err }));
                    return;
                }
                ws.send(JSON.stringify({ api: "detach", code: 0 }));
            })
        } else if (api == "bind") {
            if (!data) {
                ws.send(JSON.stringify({ api: "bind", code: -1, data: 'Invalid parameter' }));
                return;
            }
            usbip_bind(data, (err) => {
                if (err) {
                    ws.send(JSON.stringify({ api: "bind", code: -1, data: err }));
                    return;
                }
                ws.send(JSON.stringify({ api: "bind", code: 0 }));
            })
        } else if (api == "unbind") {
            if (!data) {
                ws.send(JSON.stringify({ api: "unbind", code: -1, data: 'Invalid parameter' }));
                return;
            }
            usbip_unbind(data, (err) => {
                if (err) {
                    ws.send(JSON.stringify({ api: "unbind", code: -1, data: err }));
                    return;
                }
                ws.send(JSON.stringify({ api: "unbind", code: 0 }));
            })
        } else if (api == "refresh") {
            if (!data || data > 3) {
                ws.send(JSON.stringify({ api: "refresh", code: -1, data: 'Invalid parameter' }));
                return;
            }
            switch (data) {
                case 0: ticks.local = 0; break;
                case 1: ticks.port = 0; break;
                case 2: ticks.server = 0; break;
                case 3: Object.keys(ticks.remote).map((v, k) => { ticks.remote[k] = 0; }); break;
                default: break;
            }
            ws.send(JSON.stringify({ api: "refresh", code: 0 }));
        } else {
            ws.send(JSON.stringify({ api: "error", data: 'Invalid action specified' }));
        }
    }

    let ticks = {
        local: 0,
        port: 0,
        server: 0,
        remote: {},
    }

    setInterval(() => {
        let tick = Date.now();
        if (tick - ticks.local >= 1 * 1000) {
            ticks.local = tick;
            /* 搜索 本地 USB 设备 */
            probe_local_usb_devices();
            // usbip_bind
        }
        if (!enable_only_server) {
            if (sys_type != 'win32') {
                if (tick - ticks.port >= 1 * 1000) {
                    ticks.port = tick;
                    /* 搜索 已挂载的网络 USB 设备 */
                    probe_usbip_port();
                }
                if (ticks.server != true) {
                    ticks.server = true;
                    /* 搜索 局域网内 USB 共享服务器 */
                    // 创建 Worker 线程
                    const worker = new Worker(__filename, {
                        workerData: { ip_list }
                    });
                    // let start = Date.now();

                    // 监听 Worker 线程返回的消息
                    worker.on('message', (result) => {
                        ticks.server = false;
                        // console.log(Date.now() - start)
                        if (JSON.stringify(result) != JSON.stringify(usbip.servers)) {
                            console.log(`USBIP Servers`, result);
                            ws_send_msg_to_all({ api: "servers", data: result })
                            usbip.servers = result;
                        }
                    });
                }

                for (let ip in usbip.servers) {
                    if (usbip.servers[ip]) {
                        if (!ticks.remote[ip])
                            ticks.remote[ip] = 0;
                        if (tick - ticks.remote[ip] >= PROBE_INTERVAL) {
                            ticks.remote[ip] = tick;
                            /* 搜索 远程 USB 设备 */
                            probe_remote_usb_list(ip);
                        }
                    }
                }
            }
        }
    }, 3000);

    function usbip_attach(server, id, cb) {
        // let cmd =
        //     sys_type == 'win32' ?
        //         `usbipd attach -a --remote=${server} -b ${id}` :
        //         `${sudo_str} usbip attach --remote=${server} -b ${id}`
        exec(`${sudo_str} usbip attach --remote=${server} -b ${id}`, (err, stdout, stderr) => {
            if (cb) {
                if (err)
                    cb(stderr);
                else
                    cb();
            }
        });
    }

    function usbip_detach(port, cb) {
        exec(`${sudo_str} usbip detach --port=${port}`, (err, stdout, stderr) => {
            if (cb) {
                if (err)
                    cb(stderr);
                else {
                    probe_remote_usb_list(usbip.port[port].server)
                    cb();
                }
            }
        });
    }

    function usbip_bind(id, cb) {
        exec(`${sudo_str} usbip bind -b ${id}`, (err, stdout, stderr) => {
            if (err) {
                if (stderr.indexOf("is already") != -1) {
                    if (cb)
                        cb();
                    usbip.bind[id] = true;
                } else
                    if (cb)
                        cb(stderr);
            }
            else {
                if (cb)
                    cb();
                usbip.bind[id] = true;
            }
        });
    }

    function usbip_unbind(id, cb) {
        exec(`${sudo_str} usbip unbind -b ${id}`, (err, stdout, stderr) => {
            if (cb) {
                if (err) {
                    if (stderr.indexOf("is not bound") != -1) {
                        cb();
                        delete usbip.bind[id];
                    } else
                        cb(stderr);
                }
                else {
                    cb();
                    delete usbip.bind[id];
                }
            }
        });
    }

    async function probe_local_usb_devices() {
        // console.log("probe local usb devices");
        exec(`usbip list -l`, (err, stdout, stderr) => {
            let devices = {};
            if (stderr.length > 0) {
                if (JSON.stringify(devices) != JSON.stringify(usbip.devices.local)) {
                    console.log("Local USB Devices : ", devices)
                    ws_send_msg_to_all({ api: "local", data: devices })
                }
                usbip.devices.local = devices;
                return;
            }
            let devices_array = stdout.split("\n\n");
            for (let idx = 0; idx < devices_array.length; idx++) {
                let res = devices_array[idx].split("\n");
                if (res.length > 1) {
                    let id_addr_array = res[0].trim().split(" ");
                    let busid = id_addr_array[2].trim();
                    let address = id_addr_array[3].trim();
                    address = address.slice(1);
                    address = address.slice(0, -1);
                    let described_array = res[1].trim().split(":");
                    let vendor = described_array[0].trim();
                    let product = described_array[1].trim().split(" ")[0];
                    let described = described_array[1].split("(")[0].trim();
                    devices[busid] = {
                        id: busid,
                        address,
                        vendor,
                        product,
                        described,
                        raw: devices_array[idx]
                    }
                    if (!usbip.devices.local[busid]) {
                        console.log(`New Local Device ${busid}`, devices[busid]);
                        if (enable_auto_mount) {
                            if(excluded_list.indexOf(address) == -1)
                            {
                                console.log(`Share local device ${busid} `)
                                usbip_bind(busid);
                            }
                        }
                    }
                }
            }
            if (JSON.stringify(devices) != JSON.stringify(usbip.devices.local)) {
                console.log("Local USB Devices : ", devices)
                ws_send_msg_to_all({ api: "local", data: devices })
            }
            usbip.devices.local = devices;
        });
    }

    async function probe_usbip_port() {
        // console.log("probe usbip port");
        function check_port_change(new_ports) {
            if (JSON.stringify(new_ports) != JSON.stringify(usbip.port)) {
                console.log("Port Devices : ", new_ports)
                ws_send_msg_to_all({ api: "ports", data: new_ports })
            }
            usbip.port = new_ports;
        }
        exec(`${sudo_str} usbip port`, (err, stdout, stderr) => {
            if (stderr.length > 0) {
                check_port_change({});
                console.log(err, stderr, stdout)
                return;
            }
            let ports_array = stdout.split("\n");
            for (let i = 0; i < ports_array.length; i++) {
                if (ports_array[i].indexOf('Port') != -1) {
                    ports_array = ports_array.slice(i);
                    break;
                }
            }
            if (ports_array.length < 4) {
                check_port_change({});
                return;
            }
            let ports = {};
            for (let i = 0; i < ports_array.length; i += 4) {
                if (ports_array[i].length == 0)
                    continue;
                let port_str = ports_array[i].trim();
                let vendor_product_str = ports_array[i + 1].trim();
                let id_str = ports_array[i + 2].trim();
                let info_str = ports_array[i + 3].trim();

                let id = port_str.split(":")[0].split(" ")[1];
                let speed = port_str.split("(")[1].slice(0, -1);
                let attach_id = id_str.split(" -> ")[0];
                let server = id_str.split(":")[1].slice(2);
                let device_id = id_str.split("/");
                device_id = device_id[device_id.length - 1];
                let vendor = vendor_product_str.split(":")[0].trim();
                let product = vendor_product_str.split(":")[1].split("(")[0].trim();
                let address = vendor_product_str.split("(")[1].slice(0, -1);

                ports[id] = {
                    id,
                    speed,
                    attach_id,
                    server,
                    device_id,
                    vendor,
                    product,
                    address,
                    raw: ports_array[i]
                }

                if (!usbip.port[id]) {
                    console.log(`New Port device ${id}`, ports[id]);
                }
            }
            check_port_change(ports);
        });
    }

    async function probe_remote_usb_list(remote_ip) {
        exec(`usbip list -r ${remote_ip}`, (err, stdout, stderr) => {
            let res = stdout.split("\n\n");
            let remote_list = {};
            for (let idx in res) {
                let device_array = res[idx].split("\n");
                if (device_array.length < 2)
                    continue;
                if (idx == 0)
                    device_array = device_array.slice(3);
                let id_info_array = device_array[0].trim().split(":");
                let usb_infos = {
                    id: id_info_array[0],
                    vendor: id_info_array[1].trim(),
                    product: id_info_array[2].split("(")[0].trim(),
                    address: id_info_array[2].split("(")[1] + ":" + id_info_array[3].trim().slice(0, -1),
                    described: [],
                    raw: device_array.join("\n")
                }
                for (let i = 1; i < device_array.length; i++)
                    usb_infos.described.push(device_array[i].split(':')[1].trim())
                remote_list[usb_infos.id] = usb_infos;
                if (usbip.devices.remote[remote_ip] && !usbip.devices.remote[remote_ip][usb_infos.id]) {
                    console.log(`New Server${remote_ip} Devices ${usb_infos.id}`, usb_infos);
                }
            }
            if (JSON.stringify(remote_list) != JSON.stringify(usbip.devices.remote[remote_ip])) {
                console.log(`Server ${remote_ip} Devices : `, remote_list)
                ws_send_msg_to_all({ api: "devices", data: { ip: remote_ip, list: remote_list } })
            }
            usbip.devices.remote[remote_ip] = remote_list;
        });
    }
} else {
    const { ip_list } = workerData;
    let list = {};
    console.log("probe remote usbip server", ip_list);

    async function probe_remote_ip(target_ip) {
        // return new Promise((resolve, reject) => {
        //     exec(`usbip list -r ${target_ip}`, (err, stdout, stderr) => {
        //         if (stderr.indexOf("could not connect to") != -1) {
        //             resolve(false)
        //         } else {
        //             resolve(true)
        //         }
        //     });
        // });
        return new Promise((resolve, reject) => {
            const client = new net.Socket();
            const USBIP_PORT = 3240;
            client.setTimeout(100)
            client.connect(USBIP_PORT, target_ip, () => {
                // console.log(`connect ${target_ip}:${USBIP_PORT}`)
                client.destroy()
                resolve(true)
            });
            client.on('error', (err) => {
                resolve(false)
            });
            client.on('timeout', (err) => {
                client.destroy()
                resolve(false)
            });
        });
    }

    async function probe_remote_slice(probe_net, start, end) {
        // return new Promise(async (resolve, reject) => {
        let ip = probe_net;
        for (let p = start; p < end; p++) {
            ip[3] = p;
            let target_ip = ip.join('.');
            let con = false
            for (let i = 0; i < ip_list.length; i++) {
                if (ip_list[i] == target_ip) {
                    con = true;
                    break;
                }
            }
            if (con)
                continue;
            let result = await probe_remote_ip(target_ip);
            if (result) {
                console.log(`probe usbip server ${target_ip}`);
                list[target_ip] = true;
            }
            // else
            //     console.log(`${target_ip} no usbip server `);
        }
        //     resolve(true);
        // });
    }

    async function probe_remote(target_ip) {
        let probe_nets = {};
        for (let i = 0; i < ip_list.length; i++) {
            let ip = ip_list[i].split(".");
            let probe_net = ip.slice(0, -1).join(".");
            if (probe_nets[probe_net])
                continue;
            probe_nets[probe_net] = true;
            Promise.all([
                new Promise(async (resolve, reject) => {
                    await probe_remote_slice(ip, 2, 50);
                    resolve(true);
                }),
                new Promise(async (resolve, reject) => {
                    await probe_remote_slice(ip, 51, 100);
                    resolve(true);
                }),
                new Promise(async (resolve, reject) => {
                    await probe_remote_slice(ip, 101, 150);
                    resolve(true);
                }),
                new Promise(async (resolve, reject) => {
                    await probe_remote_slice(ip, 151, 200);
                    resolve(true);
                }),
                new Promise(async (resolve, reject) => {
                    await probe_remote_slice(ip, 201, 255);
                    resolve(true);
                })
            ]).then((values) => {
                console.log("result:", list);
                parentPort.postMessage(list);  // 向主线程发送结果
            });

            // for (let p = 2; p < 255; p++) {
            //     ip[3] = p;
            //     let target_ip = ip.join('.');
            //     let con = false
            //     for (let j = 0; j < ip_list.length; j++) {
            //         if (ip_list[j] == target_ip) {
            //             con = true;
            //             break;
            //         }
            //     }
            //     if (con)
            //         continue;
            //     let result = await probe_remote_ip(target_ip);
            //     if (result) {
            //         console.log(`probe usbip server ${target_ip}`);
            //         list[target_ip] = true;
            //     }
            //     if (i + 1 >= ip_list.length && p >= 254) {
            //         console.log("result:", list);
            //         parentPort.postMessage(list);  // 向主线程发送结果
            //     }
            //     // exec(`usbip list -r ${target_ip}`, (err, stdout, stderr) => {
            //     //     if (stderr.indexOf("could not connect to") != -1) {
            //     //         // if (usbip.servers[target_ip] == true)
            //     //         //     console.log(`${target_ip} disconnect`)
            //     //         // usbip.servers[target_ip] = false;
            //     //     } else {
            //     //         // if (stderr.indexOf("no exportable devices found on") == -1)
            //     //         //     console.log('stdout:', stdout, 'stderr:', stderr)
            //     //         console.log(`probe usbip server ${target_ip}`);
            //     //         list[target_ip] = true;
            //     //     }
            //     //     if (i + 1 >= ip_list.length && p >= 254) {
            //     //         console.log("result:", list);
            //     //         parentPort.postMessage(list);  // 向主线程发送结果
            //     //     }
            //     // });
            // }
        }
    }
    probe_remote();
}